package com.faxsun.core.payment.service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;

import org.apache.commons.lang3.StringUtils;
import org.broadleafcommerce.common.payment.PaymentAdditionalFieldType;
import org.broadleafcommerce.common.payment.PaymentGatewayType;
import org.broadleafcommerce.common.payment.PaymentType;
import org.broadleafcommerce.common.payment.dto.GatewayCustomerDTO;
import org.broadleafcommerce.common.payment.dto.PaymentResponseDTO;
import org.broadleafcommerce.common.payment.service.PaymentGatewayConfiguration;
import org.broadleafcommerce.core.payment.domain.OrderPayment;
import org.broadleafcommerce.core.payment.domain.PaymentTransaction;
import org.broadleafcommerce.core.payment.service.DefaultPaymentGatewayCheckoutService;
import org.broadleafcommerce.profile.core.domain.Address;
import org.broadleafcommerce.profile.core.domain.Customer;

import com.faxsun.core.order.domain.FSOrderImpl;
import com.faxsun.core.order.service.type.FSInternalOrderStatus;
import com.faxsun.core.order.service.type.FSOrderStatus;

public class FSDefaultPaymentGatewayCheckoutService extends DefaultPaymentGatewayCheckoutService {

	@Override
	public Long applyPaymentToOrder(PaymentResponseDTO responseDTO, PaymentGatewayConfiguration config) {

		// Payments can ONLY be parsed into Order Payments if they are 'valid'
		if (!responseDTO.isValid()) {
			throw new IllegalArgumentException(
					"Invalid payment responses cannot be parsed into the order payment domain");
		}

		if (config == null) {
			throw new IllegalArgumentException("Config service cannot be null");
		}

		Long orderId = Long.parseLong(responseDTO.getOrderId());
		FSOrderImpl order = (FSOrderImpl) orderService.findOrderById(orderId);

		// if (!FSOrderStatus.IN_PROCESS.equals(order.getStatus()) &&
		// !FSOrderStatus.CSR_OWNED.equals(order.getStatus())) {
		if (!FSInternalOrderStatus.PAYMENT_SUBMITTED.equals(order.getInternalStatus())
				&& !FSInternalOrderStatus.PAYMENT_IN_PROCESS.equals(order.getInternalStatus())
				&& !FSInternalOrderStatus.PAYMENT_FAIL.equals(order.getInternalStatus())
				&& !FSOrderStatus.CSR_OWNED.equals(order.getStatus())) {
			throw new IllegalArgumentException(
					"Cannot apply another payment to an Order that is not PAYMENT_SUBMITTED"
							+ "or PAYMENT_IN_PROCESS or PAYMENT_FAIL or CSR_OWNED");
		}

		Customer customer = order.getCustomer();
		if (customer.isAnonymous()) {
			GatewayCustomerDTO<PaymentResponseDTO> gatewayCustomer = responseDTO.getCustomer();
			if (StringUtils.isEmpty(customer.getFirstName()) && gatewayCustomer != null) {
				customer.setFirstName(gatewayCustomer.getFirstName());
			}
			if (StringUtils.isEmpty(customer.getLastName()) && gatewayCustomer != null) {
				customer.setLastName(gatewayCustomer.getLastName());
			}
			if (StringUtils.isEmpty(customer.getEmailAddress()) && gatewayCustomer != null) {
				customer.setEmailAddress(gatewayCustomer.getEmail());
			}
		}

		// If the gateway sends back an email address and the order does not
		// contain one, set it.
		GatewayCustomerDTO<PaymentResponseDTO> gatewayCustomer = responseDTO.getCustomer();
		if (order.getEmailAddress() == null && gatewayCustomer != null) {
			order.setEmailAddress(gatewayCustomer.getEmail());
		}

		// If the gateway sends back Shipping Information, we will save that to
		// the first shippable fulfillment group.
		populateShippingInfo(responseDTO, order);

		// ALWAYS create a new order payment for the payment that comes in.
		// Invalid payments should be cleaned up by
		// invoking {@link #markPaymentAsInvalid}.
		OrderPayment payment = orderPaymentService.create();
		payment.setType(responseDTO.getPaymentType());
		payment.setPaymentGatewayType(responseDTO.getPaymentGatewayType());
		payment.setAmount(responseDTO.getAmount());

		// If this gateway does not support multiple payments then mark all of
		// the existing payments
		// as invalid before adding the new one
		List<OrderPayment> paymentsToInvalidate = new ArrayList<OrderPayment>();
		Address tempBillingAddress = null;
		if (!config.handlesMultiplePayments()) {
			PaymentGatewayType gateway = config.getGatewayType();
			for (OrderPayment p : order.getPayments()) {
				// A Payment on the order will be invalidated if:
				// - It's a temporary order payment: There may be a temporary
				// Order Payment on the Order (e.g. to save the billing address)
				// - The payment being added is a Final Payment and there
				// already exists a Final Payment
				// - The payment being added has the same gateway type of an
				// existing one.
				if (PaymentGatewayType.TEMPORARY.equals(p.getGatewayType())
						|| (p.isFinalPayment() && payment.isFinalPayment())
						|| (p.getGatewayType() != null && p.getGatewayType().equals(gateway))) {

					paymentsToInvalidate.add(p);

					if (PaymentType.CREDIT_CARD.equals(p.getType())
							&& PaymentGatewayType.TEMPORARY.equals(p.getGatewayType())) {
						tempBillingAddress = p.getBillingAddress();
					}
				}
			}
		}

		for (OrderPayment invalid : paymentsToInvalidate) {
			order.getPayments().remove(invalid);
			markPaymentAsInvalid(invalid.getId());
		}

		// The billing address that will be saved on the order will be parsed
		// off the
		// Response DTO sent back from the Gateway as it may have Address
		// Verification or Standardization.
		// If you do not wish to use the Billing Address coming back from the
		// Gateway, you can override the
		// populateBillingInfo() method or set the useBillingAddressFromGateway
		// property.
		populateBillingInfo(responseDTO, payment, tempBillingAddress);

		// Create the transaction for the payment
		PaymentTransaction transaction = orderPaymentService.createTransaction();
		transaction.setAmount(responseDTO.getAmount());
		transaction.setRawResponse(responseDTO.getRawResponse());
		transaction.setSuccess(responseDTO.isSuccessful());
		transaction.setType(responseDTO.getPaymentTransactionType());
		for (Entry<String, String> entry : responseDTO.getResponseMap().entrySet()) {
			transaction.getAdditionalFields().put(entry.getKey(), entry.getValue());
		}

		// Set the Credit Card Info on the Additional Fields Map
		if (PaymentType.CREDIT_CARD.equals(responseDTO.getPaymentType())
				&& responseDTO.getCreditCard().creditCardPopulated()) {

			transaction.getAdditionalFields().put(PaymentAdditionalFieldType.NAME_ON_CARD.getType(),
					responseDTO.getCreditCard().getCreditCardHolderName());
			transaction.getAdditionalFields().put(PaymentAdditionalFieldType.CARD_TYPE.getType(),
					responseDTO.getCreditCard().getCreditCardType());
			transaction.getAdditionalFields().put(PaymentAdditionalFieldType.EXP_DATE.getType(),
					responseDTO.getCreditCard().getCreditCardExpDate());
			transaction.getAdditionalFields().put(PaymentAdditionalFieldType.LAST_FOUR.getType(),
					responseDTO.getCreditCard().getCreditCardLastFour());
		}

		// TODO: validate that this particular type of transaction can be added
		// to the payment (there might already
		// be an AUTHORIZE transaction, for instance)
		// Persist the order payment as well as its transaction
		payment.setOrder(order);
		transaction.setOrderPayment(payment);
		payment.addTransaction(transaction);
		payment = orderPaymentService.save(payment);

		if (transaction.getSuccess()) {
			orderService.addPaymentToOrder(order, payment, null);
		} else {
			// We will have to mark the entire payment as invalid and boot the
			// user to re-enter their
			// billing info and payment information as there may be an error
			// either with the billing address/or credit card
			handleUnsuccessfulTransaction(payment);
		}

		return payment.getId();
	}
}
